Skip to content

Conversation

RafaelGSS
Copy link
Member

@RafaelGSS RafaelGSS commented Sep 5, 2025

Backportable version of #59747

cc: @ronag

image

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/http
  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added http Issues or PRs related to the http subsystem. needs-ci PRs that need a full CI run. labels Sep 5, 2025
Copy link

codecov bot commented Sep 5, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.43%. Comparing base (24ded11) to head (29753ac).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #59778      +/-   ##
==========================================
- Coverage   88.45%   88.43%   -0.03%     
==========================================
  Files         703      703              
  Lines      207544   207577      +33     
  Branches    40013    40016       +3     
==========================================
- Hits       183587   183574      -13     
- Misses      15965    15996      +31     
- Partials     7992     8007      +15     
Files with missing lines Coverage Δ
lib/_http_incoming.js 99.35% <100.00%> (+0.01%) ⬆️
lib/_http_server.js 97.35% <100.00%> (-0.03%) ⬇️

... and 46 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nodejs-github-bot
Copy link
Collaborator

@ronag ronag requested a review from mcollina September 6, 2025 12:56
const fastDump = options.fastDump;
if (fastDump !== undefined)
validateBoolean(fastDump, 'options.fastDump');
this[kfastDump] = fastDump || false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OR seems redudant since we are passing through validateBoolean

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? if fastDump is undefined, this[kfastDump] will be undefined instead of false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could shortcut that just a bit by assigning a default:

const { fastDump = false } = options.fastDump;
validateBoolean(fastDump, 'options.fastDump');
this[kfastDump] = fastDump;

@pimterry
Copy link
Member

pimterry commented Sep 8, 2025

Thinking more about the heuristics mentioned at the end #59747, it looks like we can go further and do even better here, very easily.

I think we can do this for all methods, quickly & safely, because it's already impossible to receive a request with a body without it being indicated it in the headers (content-length or transfer-encoding), even in the current implementation, even for POST and others etc.

On Node v24 right now, if you send:

POST / HTTP/1.1
Host: test

message body

Then Node currently fires request and end (but no data), and then returns a 400 response to the unparseable message body (which it expects to be $METHOD $PATH HTTP/1.1 starting a new request). That's because the RFC possibilities for sending request bodies always require either content-length or transfer-encoding to be set to warn you there's a body coming, and our parsing already requires this as well.

That means we could safely apply this optimization to all cases, whenever there's no content-length or transfer-encoding header set (we could look for chunked or check for != 0, but since it's primarily a performance optimization I think a quicker simpler check is fine to start with). We already have the headers parsed here so that's very quick & easy. That lets us apply the same performance boost to POSTs and all other requests with an empty body (which is reasonably common I think - there's plenty of REST APIs where the URL defines the operation, but the request is a POST because it's not safe).

This would still be breaking behaviour, because it doesn't emit end or close events, but as an opt-in option (implicitlyCloseEmptyRequests or similar) then that's fine. Or we could check for event listeners and just manually emit the events if any are registered, in which case this wouldn't be breaking at all AFAICT, so we could apply the optimization to everybody for free, but I don't know if the overhead of that negates too much of the perf boost.

@RafaelGSS
Copy link
Member Author

Good one @pimterry. I'll implement those later today and the tests asked by @Flarna.

@RafaelGSS
Copy link
Member Author

Just pushed a new version (amended) that replaces the fastDump with optimizeEmptyRequest option and as suggested by @pimterry, we'll dump the IncomingMessage whenever this option is true and when there's no content-length/transfer-encoding or the method is HEAD/GET.

PTAL.

// stream.Readable life cycle rules. The downside is that this will
// break some servers that read bodies for methods that don't have body headers.
req._dumped = true;
req._readableState.ended = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move the modifications of stream internals into stream files?
I wonder also that endEmitted/ closeEmitted are set here. Are they really emitted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder also that endEmitted/ closeEmitted are set here. Are they really emitted?

They are as if they were already emitted.

@RafaelGSS
Copy link
Member Author

RafaelGSS commented Sep 9, 2025

Applied the suggestions and added a test to guarantee .end event isn't emitted if the flag is true. I will do a second round of performance comparison on Fastify (Express doesn't seem to use req object as I thought it would use it - so the result wasn't so dramatic).

PTAL @Flarna @pimterry

@RafaelGSS RafaelGSS added the semver-minor PRs that contain new features and should be released in the next minor version. label Sep 9, 2025
@RafaelGSS RafaelGSS changed the title http: optimize IncomingMessage._dump http: add optimizeEmptyRequests option for IncomingMessage._dump Sep 9, 2025
@Flarna
Copy link
Member

Flarna commented Sep 9, 2025

@lpinca commented on the previous PR, maybe worth to ask them for a look a this one.

@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Sep 11, 2025
@nodejs-github-bot

This comment was marked as outdated.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this new option is not easy to turn on by default because it's blocking a valid case, the HTTP/1.0 case where a request is made and the socket half closed to signal its termination. I would rather have it enabled based on the HTTP method instead.

@pimterry
Copy link
Member

it's blocking a valid case, the HTTP/1.0 case where a request is made and the socket half closed to signal its termination.

@mcollina I don't think that case exists (or has ever existed). You might be thinking of the response case, where you can use connection closure to delimit bodies without using any framing headers? That's not possible for requests though, and response behaviour doesn't affect this PR. For requests, the HTTP/1.0 RFC says:

HTTP/1.0 requests containing an entity body must include a valid Content-Length header field.

and later:

Closing the connection cannot be used to indicate the end of a request body, since it leaves no possibility for the server to send back a response. Therefore, HTTP/1.0 requests containing an entity body must include a valid Content-Length header field.

I'm not even sure it would be technically possible - if you could just send content + TCP half-close to send a request body, there would be know way to ever know if a request was finished without the half-close. On any still-open connection, there could always be more request body to come.

I'd be very interested to see a counter-example, but I've just tested with a bunch of raw requets via socat and I can't see any way to make Node currently accept a request body like this, so I think we can ignore it. Same applies for HTTP/0.9.


Unfortunately this new option is not easy to turn on by default

Totally agree we can't turn this on by default anyway though, since it's a significant breaking change that will affect a lot of web frameworks & middleware etc and cause very confusing issues.

It would be great to find a way to enable this everywhere, but doing that without breakage would require emulating the actual stream behaviour (at the least, firing readable & end & close events, and probably simulating the initial paused-readable behaviour as well) and I suspect that would negate a lot of the benefit. We could explore this separately in future later if it seems worthwhile, it would definitely be good to do if we can find a way.

In the meantime, releasing the current approach allows the ecosystem to start adding the readableEnded logic to support this optimization safely now, with no downside, and if that happens widely enough we might be able to enable as-is in the distant future.

@pimterry
Copy link
Member

All LGTM now (thanks @RafaelGSS!).

Just one last thing worth noting here: when this optimization is applied, it means you also can't listen to req.on('close'), which is currently the standard way to spot aborted requests where the connection is closed before the response is completed, so you can potentially skip processing requests the client is no longer interested in.

I think that's fine (you can listen for close on the response instead, or on the socket (HTTP/1) or stream (HTTP/2) directly) but it's worth being aware of.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot

This comment was marked as outdated.

@bjohansebas
Copy link
Member

bjohansebas commented Sep 14, 2025

on-finished would technically break with these changes, since it's not detecting when IncomingMessage has completed (https://github.com/jshttp/on-finished/blob/d2974f5a18f468ea56f58acb2f6d402f4b5142f0/index.js#L92). Do you know of any patch to fix it from on-finished and also be able to complete expressjs/express#6742?

}
res.writeHead(200);
res.end('ok');
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's not clear in this test what "be optimized" means. Expanding on some code comments could help provide context to anyone coming into this code later.

@nodejs-github-bot

This comment was marked as outdated.

@nodejs-github-bot
Copy link
Collaborator

@RafaelGSS
Copy link
Member Author

RafaelGSS commented Sep 16, 2025

PTAL @pimterry @mcollina @jasnell @ronag I had to push a small fix

@RafaelGSS RafaelGSS added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Sep 16, 2025
doc/api/http.md Outdated
or `Transfer-Encoding` headers (indicating no body) will be initialized with an
already-ended body stream, so they will never emit any stream events
(like `'data'` or `'end'`). You can use `req.readableEnded` to detect this case.
This option is still under experimental phase.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is indeed true, we should emit an experimental warning whenever this flag is used. If this is no longer correct, we should remove this comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think emitting a warning for this specific option would be good. I imagine this being enabled on frameworks like fastify and express, and having such a warning might point users away. I am confident that the documentation note is enough for now.

What do you think? @mcollina @ronag

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Than I recommend emitting a warning if it's not inside node_modules?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the experimental documentation here only for the next two v24 releases at least, just in case we are missing some specific edge case that would completely change how this flag works - you know, touching on http usually breaks people in many ways. Ideally, this line should be removed in a couple of releases.

I'm not sure if it's worth it to consider such a warning since we'll remove it very soon.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anonrig I have removed the experimental comment.

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@RafaelGSS
Copy link
Member Author

Ping @anonrig

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. http Issues or PRs related to the http subsystem. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.